'use strict'
const fs = require('fs')
const path = require('path')
const userHome = require('os').homedir()
const pathExists = require('path-exists').sync
const pkgUp = require('pkg-up')
const fse = require('fs-extra')
const glob = require('glob')
const ejs = require('ejs')
const semver = require('semver')
const Command = require('@kpzc-cli-dev/command')
const Package = require('@kpzc-cli-dev/package')
const request = require('@kpzc-cli-dev/request')
const log = require('@kpzc-cli-dev/log')
const { spinnerStart, sleep, execAsync } = require('@kpzc-cli-dev/utils')
const inquirer = require('inquirer')
const ADD_MODE_SECTION = 'section'
const ADD_MODE_PAGE = 'page'
const TYPE_CUSTOM = 'custom'
const TYPE_NORMAL = 'normal'

process.on('unhandledRejection', (e) => {})

class AddCommand extends Command {
  init() {
    // 获取add命令的初始化参数
  }
  async exec() {
    // 代码片段(区块)：以源码形式拷贝的vue组件
    // 1. 选择复用方式
    this.addMode = (await this.getAddMode()).addMode
    // 2. 选择代码片段模版
    if (this.addMode === ADD_MODE_SECTION) {
      await this.installSectionTemplate()
    } else {
      await this.installPageTemplate()
    }
  }
  async getPageTemplate() {
    return request({
      url: '/page/template',
      method: 'get'
    })
  }
  async getSectionTemplate() {
    return request({
      url: '/section/template',
      method: 'get'
    })
  }
  // 安装代码片段
  async installSectionTemplate() {
    // 1. 获取页面安装目录
    this.dir = process.cwd()
    // 2. 选择页面模版
    this.sectionTemplate = await this.getTemplate(ADD_MODE_SECTION)
    // 3. 安装代码片段模版
    // 3.1 检查目录重名问题
    await this.prepare(ADD_MODE_SECTION)
    // 3.2 代码片段模版下载
    await this.downloadTemplate(ADD_MODE_SECTION)
    // 3.3 代码片段的安装
    await this.installSection()
  }
  async installSection() {
    // 1. 选择要插入的源码文件
    let files = fs.readdirSync(this.dir, { withFileTypes: true })
    files = files
      .map((file) => (file.isFile() ? file.name : null))
      .filter((_) => _)
      .map((file) => ({
        name: file,
        value: file
      }))
    if (files.length === 0) {
      throw new Error('当前文件夹下没有文件！')
    }
    const { codeFile } = await inquirer.prompt({
      type: 'list',
      message: '请选择要插入代码的源码文件',
      name: 'codeFile',
      choices: files
    })
    // 2. 用户输入插入的行数
    const { lineNumber } = await inquirer.prompt({
      type: 'input',
      message: '请输入要插入行数',
      name: 'lineNumber',
      validate: function (v) {
        const done = this.async()
        if (!v || !v.trim()) {
          done('输入要插入行数不能为空！')
          return
        } else if (v >= 0 && Math.floor(v) === Number(v)) {
          done(null, true)
        } else {
          done('插入行数必须是整数！')
          return
        }
      }
    })
    log.verbose('codeFile', codeFile)
    log.verbose('lineNumber', lineNumber)
    // 3. 对源码文件进行分割成数组
    const codeFilePath = path.resolve(this.dir, codeFile)
    const codeContent = fs.readFileSync(codeFilePath, 'utf-8')
    let codeContentArr = codeContent.split('\n')
    // 4. 以组件形式插入代码片段
    const componentName = this.sectionTemplate.sectionName.toLocaleLowerCase()
    const componentNameOrigin = this.sectionTemplate.sectionName
    codeContentArr.splice(
      lineNumber,
      0,
      `<${componentName}></${componentName}>`
    )
    // 4. 插入代码片段import语句
    const scriptIndex = codeContentArr.findIndex(
      (code) => code.replace(/\s/g, '') === '<script>'
    )
    codeContentArr.splice(
      scriptIndex + 1,
      0,
      `import ${componentNameOrigin} from './components/${componentNameOrigin}/Index.vue'`
    )
    // 将代码还原为string
    const newCodeContent = codeContentArr.join('\n')
    fs.writeFileSync(codeFilePath, newCodeContent, 'utf-8')
    log.success('代码片段写入成功')
    // 创建代码片段组件目录
    fse.ensureDirSync(this.targetPath)
    const templatePath = path.resolve(
      this.sectionTemplatePackage.cacheFilePath,
      'template',
      this.sectionTemplate.targetPath ? this.sectionTemplate.targetPath : ''
    )
    const targetPath = this.targetPath
    fse.copySync(templatePath, targetPath)
    log.success('代码片段拷贝成功')
  }
  async installPageTemplate() {
    // 1. 获取页面的安装路径
    this.dir = process.cwd()
    // 2. 选择页面模版
    this.pageTemplate = await this.getTemplate()
    // 3.0 预先检查目录重名问题
    await this.prepare(ADD_MODE_PAGE)
    // 3. 安装页面模版
    await this.downloadTemplate(ADD_MODE_PAGE)
    // 3.1 下载页面模版到缓存目录
    // 3.2 将页面模版拷贝到指定目录
    // 4. 合并页面模版依赖
    // 5. 页面模版安装完成
    await this.installTemplate()
  }
  async getAddMode() {
    return inquirer.prompt({
      type: 'list',
      name: 'addMode',
      message: '请选择代码复用模式',
      choices: [
        {
          name: '代码片段',
          value: ADD_MODE_SECTION
        },
        {
          name: '页面模版',
          value: ADD_MODE_PAGE
        }
      ]
    })
  }
  async installTemplate() {
    log.info('正在安装页面模版')
    log.verbose('pageTemplate', this.pageTemplate)
    // 模版路径
    const templatePath = path.resolve(
      this.pageTemplatePackage.cacheFilePath,
      'template',
      this.pageTemplate.targetPath
    )
    if (!pathExists(templatePath)) {
      throw new Error('页面模版不存在！')
    }
    // 目标路径
    const targetPath = this.targetPath
    log.verbose('templatePath', templatePath)
    log.verbose('targetPath', targetPath)
    fse.ensureDirSync(templatePath)
    fse.ensureDirSync(targetPath)
    if (this.pageTemplate.type === TYPE_CUSTOM) {
      await this.installCustomPageTemplate({ templatePath, targetPath })
    } else {
      await this.installNormalPageTemplate({ templatePath, targetPath })
    }
  }
  async installCustomPageTemplate({ templatePath, targetPath }) {
    // 获取自定义模版的入口文件
    const rootFile = this.pageTemplatePackage.getRootFilePath()
    if (fs.existsSync(rootFile)) {
      console.log(rootFile)
      log.notice('开始执行自定义模版')
      const options = {
        templatePath,
        targetPath,
        pageTemplate: this.pageTemplate
      }
      const code = `require('${rootFile}')(${JSON.stringify(options)})`
      await execAsync('node', ['-e', code], {
        stdio: 'inherit',
        cwd: process.cwd()
      })
      log.success('自定义模版安装成功')
    } else {
      throw new Error('自定义模版入口文件不存在！')
    }
  }
  async installNormalPageTemplate({ templatePath, targetPath }) {
    fse.copySync(templatePath, targetPath)
    await this.ejsRender({ targetPath })
    await this.dependenciesMerge({ templatePath, targetPath })
    log.success('安装页面模版成功')
  }
  async ejsRender(options) {
    const { targetPath } = options
    const pageTemplate = this.pageTemplate
    const { ignore } = pageTemplate
    return new Promise((resolve, reject) => {
      glob(
        '**',
        {
          cwd: targetPath,
          nodir: true,
          ignore: ignore || ''
        },
        (err, files) => {
          log.verbose('files', files)
          if (err) {
            reject(err)
          } else {
            Promise.all(
              files.map((file) => {
                // 获取文件真实路径
                const filePath = path.resolve(targetPath, file)
                return new Promise((resolve1, reject1) => {
                  // ejs渲染，重新拼接render参数
                  ejs.renderFile(
                    filePath,
                    {
                      name: pageTemplate.pageName.toLocaleLowerCase()
                    },
                    {},
                    (err, result) => {
                      if (err) {
                        reject1(err)
                      } else {
                        // 重新写入文件信息
                        fse.writeFileSync(filePath, result)
                        resolve1(result)
                      }
                    }
                  )
                })
              })
            )
              .then(() => {
                resolve()
              })
              .catch((e) => {
                reject(e)
              })
          }
        }
      )
    })
  }

  async dependenciesMerge(options) {
    function objToArray(o) {
      const arr = []
      Object.keys(o).forEach((key) => {
        arr.push({
          key,
          value: o[key]
        })
      })
      return arr
    }
    function depDiff(templateDepArr, targetDepArr) {
      const finalDep = [...targetDepArr]
      // 1. 场景 模版中存在依赖，项目中不存在 （拷贝依赖）
      // 2. 场景 模版中存在，项目中也存在依赖（不会拷贝依赖， 但是在脚手架中提示，让开发者手动修改依赖）
      templateDepArr.forEach((templateDep) => {
        const duplicateDep = targetDepArr.find(
          (targetDep) => templateDep.key === targetDep.key
        )
        if (duplicateDep) {
          log.verbose('查询到重复依赖：', duplicateDep)
          const templateRange = semver
            .validRange(templateDep.value)
            .split('<')[1]
          const targetRange = semver
            .validRange(duplicateDep.value)
            .split('<')[1]
          if (templateRange !== targetRange) {
            log.warn(
              `${templateDep.key}冲突，${templateDep.value} => ${duplicateDep.value}`
            )
            return
          }
        } else {
          log.verbose('查询到新依赖：', templateDep)
          finalDep.push(templateDep)
        }
      })
      return finalDep
    }
    function arrToObj(arr) {
      let obj = {}
      arr.forEach((item) => {
        obj[item.key] = item.value
      })
      return obj
    }
    // 处理依赖合并问题
    // 1. 获取package.json
    const { templatePath, targetPath } = options
    const templatePkgPath = pkgUp.sync({ cwd: templatePath })
    const targetPkgPath = pkgUp.sync({ cwd: targetPath })
    const templatePkg = fse.readJSONSync(templatePkgPath)
    const targetPkg = fse.readJSONSync(targetPkgPath)
    // 2. 获取dependencies
    const templateDep = templatePkg.dependencies || {}
    const targetDep = targetPkg.dependencies || {}
    // 3. 对象转化为数组
    const templateDepArr = objToArray(templateDep)
    const targetDepArr = objToArray(targetDep)
    // 4. dep 之间的diff
    const newDep = depDiff(templateDepArr, targetDepArr)
    // 5. 数组转化为对象
    const newDepObj = arrToObj(newDep)
    targetPkg.dependencies = newDepObj
    // 6. 写入目标目录package.json
    fse.writeJSONSync(targetPkgPath, targetPkg, { spaces: 2 })

    // 自动安装依赖
    log.verbose('项目依赖安装路径', path.dirname(targetPkgPath))
    log.info('正在安装页面模版依赖')
    await this.execCommand('npm install', path.dirname(targetPkgPath))
    log.success('安装页面模版依赖成功！')
  }
  async execCommand(command, cwd) {
    let ret
    if (command) {
      // npm install => ['npm', 'install']
      const cmdArray = command.split(' ')
      const cmd = cmdArray[0]
      const args = cmdArray.slice(1)
      ret = await execAsync(cmd, args, {
        stdio: 'inherit',
        cwd
      })
      if (ret !== 0) {
        throw new Error(command + '命令执行失败！')
      }
      return ret
    }
  }
  async prepare(addMode = ADD_MODE_PAGE) {
    // 生成最终拷贝路径
    if (addMode === ADD_MODE_PAGE) {
      this.targetPath = path.resolve(this.dir, this.pageTemplate.pageName)
    } else {
      this.targetPath = path.resolve(
        this.dir,
        'components',
        this.sectionTemplate.sectionName
      )
    }
    if (pathExists(this.targetPath)) {
      throw new Error('页面文件夹已经存在')
    }
  }
  async downloadTemplate(addMode = ADD_MODE_PAGE) {
    // 获取模版mc
    const name = addMode === ADD_MODE_PAGE ? '页面' : '代码片段'
    // 缓存文件夹
    const targetPath = path.resolve(userHome, '.kpzc-cli-dev', 'template')
    // 缓存真实路径
    const storeDir = path.resolve(targetPath, 'node_modules')
    const { npmName, version } =
      addMode === ADD_MODE_PAGE ? this.pageTemplate : this.sectionTemplate
    // 构建package对象
    const templatePackage = new Package({
      targetPath,
      storeDir,
      packageName: npmName,
      packageVersion: version
    })
    // 页面模版是否存在
    if (!(await templatePackage.exists())) {
      // 下载模版
      const spinner = spinnerStart(`正在下载${name}模版...`)
      await sleep()
      try {
        await templatePackage.install()
      } catch (e) {
        throw e
      } finally {
        spinner.stop(true)
        if (await templatePackage.exists()) {
          log.success(`下载${name}模版成功`)
          if (addMode === ADD_MODE_PAGE) {
            this.pageTemplatePackage = templatePackage
          } else {
            this.sectionTemplatePackage = templatePackage
          }
        }
      }
    } else {
      // 更新模版
      const spinner = spinnerStart(`正在更新${name}模版...`)
      await sleep()
      try {
        await templatePackage.update()
      } catch (e) {
        throw e
      } finally {
        spinner.stop(true)
        if (await templatePackage.exists()) {
          log.success(`更新${name}模版成功`)
          if (addMode === ADD_MODE_PAGE) {
            this.pageTemplatePackage = templatePackage
          } else {
            this.sectionTemplatePackage = templatePackage
          }
        }
      }
    }
  }
  async getTemplate(addMode = ADD_MODE_PAGE) {
    const name = addMode === ADD_MODE_PAGE ? '页面' : '代码片段'
    // 通过API获取页面模版
    if (addMode === ADD_MODE_PAGE) {
      const pageTemplateData = await this.getPageTemplate()
      this.pageTemplateData = pageTemplateData
    } else {
      const sectionTemplateData = await this.getSectionTemplate()
      this.sectionTemplateData = sectionTemplateData
    }
    const TEMPLATE =
      addMode === ADD_MODE_PAGE
        ? this.pageTemplateData
        : this.sectionTemplateData
    const templateName = (
      await inquirer.prompt({
        type: 'list',
        name: 'templateName',
        message: `请选择${name}模版类型`,
        choices: this.createChoices(addMode)
      })
    ).templateName
    // 2.1 输入页面名称
    const finalTemplate = TEMPLATE.find((item) => item.npmName === templateName)
    if (!finalTemplate) {
      throw new Error(`${name}模版不存在！`)
    }
    const { selectName } = await inquirer.prompt({
      type: 'input',
      name: 'selectName',
      message: `请输入${name}名称`,
      default: '',
      validate: function (v) {
        const done = this.async()
        if (!v || !v.trim()) {
          done(`请输入${name}名称`)
          return
        }
        done(null, true)
      }
    })
    if (addMode === ADD_MODE_PAGE) {
      finalTemplate.pageName = selectName.trim()
    } else {
      finalTemplate.sectionName = selectName.trim()
    }
    return finalTemplate
  }
  createChoices(addMode) {
    return addMode === ADD_MODE_PAGE
      ? this.pageTemplateData.map((item) => ({
          name: item.name,
          value: item.npmName
        }))
      : this.sectionTemplateData.map((item) => ({
          name: item.name,
          value: item.npmName
        }))
  }
}

function add(argv) {
  log.verbose('argv', argv)
  return new AddCommand(argv)
}
module.exports = add
module.exports.AddCommand = AddCommand
